using System;
using System.Globalization;

namespace Domain.Core
{
    /// <summary>
    /// Money defined in a specific type of currency.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The <see cref="Money"/> struct allows development of applications that handle
    /// various types of currency.  Money will hold the <see cref="MoneyCurrency"/> and value of the money,
    /// and ensure that two different currencies cannot be added or subtracted to eachother.
    /// </para>
    /// <para>
    /// This code was downloaded from the following page: http://decav.com/blogs/andre/archive/2006/09/06/433.aspx.
    /// </para>
    /// </remarks>
    public struct Money
    {
        #region Static
        /// <summary>
        /// The zeroed value of <see cref="Money"/>.
        /// </summary>
        public static readonly Money Zero = new Money();

        #endregion

        #region Fields

        private MoneyCurrency _currency;

        private decimal _value;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new <see cref="Money"/> in a particular currency.
        /// </summary>
        /// <param name="currency">The currency to initialize the money in</param>
        /// <param name="value">The value of the money.</param>
        public Money(MoneyCurrency currency, decimal value)
        {
            _currency = currency;
            _value = value;
        } 

        #endregion

        #region Properties

        /// <summary>
        /// Gets the currency of the <see cref="Money"/> value.
        /// </summary>
        public MoneyCurrency Currency
        {
            get { return _currency; }
        }

        /// <summary>
        /// Gets the value of the money instance.
        /// </summary>
        public decimal Value
        {
            get { return _value; }
        } 

        #endregion

        #region Public Methods

        /// <summary>
        /// Returns a string-representation of the money value.
        /// </summary>
        /// <returns>The string value of the money.</returns>
        public override string ToString()
        {
            if (this == Zero)
                return Zero._value.ToString("C");

            NumberFormatInfo info = (NumberFormatInfo)NumberFormatInfo.CurrentInfo.Clone();

            if (Currency != MoneyCurrency.Empty)
                info.CurrencySymbol = Currency.Symbol;
            else
                info.CurrencySymbol = "$";

            return Value.ToString("C2", info);
        }

        /// <summary>
        /// Formats the internal decimal value as specified, following the rules of standard
        /// decimal formatting (<see cref="NumberFormatInfo"/>).
        /// </summary>
        /// <param name="format">The format to use.</param>
        /// <returns>The string, formatted as specified</returns>
        public string ToString(string format)
        {
            if (format == null)
                throw new ArgumentNullException("format");

            NumberFormatInfo info = (NumberFormatInfo)NumberFormatInfo.CurrentInfo.Clone();
            info.CurrencySymbol = Currency.Symbol;

            return Value.ToString(format, info);
        }

        public override int GetHashCode()
        {
            return _currency.GetHashCode() | _value.GetHashCode();
        } 
        #endregion

        #region Operators - Explicit Casting

        /// <summary>
        /// Converts the <see cref="Money"/> to its <see cref="Money.Value"/> decimal.
        /// </summary>
        /// <param name="money">The money to convert.</param>
        /// <returns>The decimal value within the money.</returns>
        public static explicit operator decimal(Money money)
        {
            return money._value;
        } 

        #endregion

        #region Equality

        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;

            if (obj.GetType() != typeof (Money))
                return false;

            Money m1 = (Money) obj;

            if (m1.Value == 0 && this.Value == 0)
                return true;

            return m1._value == this._value && m1._currency == this._currency;
        }

        public static bool operator ==(Money left, Money right)
        {
            return left.Equals(right);
        }

        public static bool operator !=(Money left, Money right)
        {
            return !left.Equals(right);
        }

        public static bool operator >(Money left, Money right)
        {
            if (right.Value == 0 && left.Value > 0)
                return true;

            if (right.Value < 0 && left.Value >= 0)
                return true;

            if (left == Zero)
                return right.Value < 0;

            if (right == Zero)
                return left.Value > 0;

            if (left.Currency != right.Currency)
                throw new InvalidOperationException("Cannot compare two Money values of different currencies.");

            return left.Value > right.Value;
        }

        public static bool operator <(Money left, Money right)
        {
            if (left.Value == 0 && right.Value > 0)
                return true;

            if (left.Value < 0 && right.Value >= 0)
                return true;

            if (right == Zero)
                return left.Value < 0;

            if (left == Zero)
                return right.Value > 0;

            if (left.Currency != right.Currency)
                throw new InvalidOperationException("Cannot compare two Money values of different currencies.");

            return left.Value < right.Value;
        }

        public static bool operator >=(Money left, Money right)
        {
            if (right.Value < 0 && left.Value >= 0)
                return true;

            if (right.Value == 0 && left.Value >= 0)
                return true;

            if (left.Currency != right.Currency)
                throw new InvalidOperationException("Cannot compare two Money values of different currencies.");

            return left.Value > right.Value || left.Equals(right);
        }

        public static bool operator <=(Money left, Money right)
        {
            if (left.Value < 0 && right.Value >= 0)
                return true;

            if (left.Value == 0 && right.Value >= 0)
                return true;

            if (left.Currency != right.Currency)
                throw new InvalidOperationException("Cannot compare two Money values of different currencies.");

            return left.Value < right.Value || left.Equals(right);
        }

        #endregion

        #region Operators - Addition/Subtraction

        public static Money operator +(Money left, Money right)
        {
            if (left == Zero)
                return right;

            if (right == Zero)
                return left;

            if (left.Currency != right.Currency)
                throw new InvalidOperationException("Cannot add two Money values that are not of the same currency.");

            return new Money(left.Currency, left.Value + right.Value);
        }

        public static Money operator -(Money left, Money right)
        {
            if (left == Zero)
                return new Money(right.Currency, 0 - right.Value);

            if (right == Zero)
                return left;

            if (left.Currency != right.Currency)
                throw new InvalidOperationException(
                    "Cannot subtract two Money values that are not of the same currency.");

            return new Money(left.Currency, left.Value - right.Value);
        }

        #endregion

        #region Operators - Division

        public static Money operator /(Money money, decimal dec)
        {
            return new Money(money.Currency, money.Value/dec);
        }

        public static Money operator /(decimal dec, Money money)
        {
            return new Money(money.Currency, money.Value/dec);
        }

        public static Money operator /(Money money, int dec)
        {
            return new Money(money.Currency, money.Value/dec);
        }

        public static Money operator /(int dec, Money money)
        {
            return new Money(money.Currency, money.Value/dec);
        }

        #endregion

        #region Operators - Multiplication

        public static Money operator *(Money money, decimal dec)
        {
            return new Money(money.Currency, money.Value*dec);
        }

        public static Money operator *(decimal dec, Money money)
        {
            return new Money(money.Currency, money.Value*dec);
        }

        public static Money operator *(Money money, int dec)
        {
            return new Money(money.Currency, money.Value*dec);
        }

        public static Money operator *(int dec, Money money)
        {
            return new Money(money.Currency, money.Value*dec);
        }

        #endregion

        #region Convert Currency and Operate

        /// <summary>
        /// Adds two <see cref="Money"/> values of different <see cref="MoneyCurrency"/>
        /// together by exchanging the <see cref="Money"/> being added into this money's
        /// currency.
        /// </summary>
        /// <remarks>
        /// If the two currencies are the same, no exchange will ouccur.
        /// </remarks>
        /// <param name="toAdd">The money to convert and add to this money.</param>
        /// <param name="exchanger">The currency exchanger to use for conversion.</param>
        /// <returns>A new <see cref="Money"/> in this money's currency of both <see cref="Money"/> added together.</returns>
        public Money ExchangeAndAdd(Money toAdd, ICurrencyExchanger exchanger)
        {
            if (exchanger == null)
                throw new ArgumentNullException("exchanger");

            if (toAdd == Zero)
                return this;

            if (this == Zero)
                return toAdd;

            // If the money is of the same currency, no exchange is required.
            if (this.Currency == toAdd.Currency)
                return toAdd + this;

            return this + exchanger.ExchangeCurrency(this.Currency, toAdd);
        }

        /// <summary>
        /// Subtracts two <see cref="Money"/> values of different <see cref="MoneyCurrency"/>
        /// together by exchanging the <see cref="Money"/> being added into this money's
        /// currency.
        /// </summary>
        /// <remarks>
        /// If the two currencies are the same, no exchange will ouccur.
        /// </remarks>
        /// <param name="toSubtract">The money to convert and subtract from this money.</param>
        /// <param name="exchanger">The currency exchanger to use for conversion.</param>
        /// <returns>A new <see cref="Money"/> in this money's currency of both <see cref="Money"/> subtracted.</returns>
        public Money ExchangeAndSubtract(Money toSubtract, ICurrencyExchanger exchanger)
        {
            if (exchanger == null)
                throw new ArgumentNullException("exchanger");

            if (toSubtract == Zero)
                return this;

            if (this == Zero)
                return new Money(toSubtract.Currency, 0 - toSubtract.Value);

            // If the money is of the same currency, no exchange is required.
            if (this.Currency == toSubtract.Currency)
                return this - toSubtract;

            return this - exchanger.ExchangeCurrency(this.Currency, toSubtract);
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        /// Gets the ratio of one money to another.
        /// </summary>
        /// <param name="numerator">The numerator of the operation.</param>
        /// <param name="denominator">The denominator of the operation.</param>
        /// <returns>A decimal from 0.0 to 1.0 of the ratio between the two money values.</returns>
        public static decimal GetRatio(Money numerator, Money denominator)
        {
            if (numerator == Zero)
                return 0;

            if (denominator == Zero)
                throw new DivideByZeroException("Attempted to divide by zero!");

            if (numerator.Currency != denominator.Currency)
                throw new InvalidOperationException(
                    "Cannot compare or operate on two currencies that are not of the same Currency.");

            return numerator.Value / denominator.Value;
        }

        /// <summary>
        /// Gets the smallest money, given the two values.
        /// </summary>
        /// <param name="m1">The first money to compare.</param>
        /// <param name="m2">The second money to compare.</param>
        /// <returns>The smallest money value of the arguments.</returns>
        public static Money Min(Money m1, Money m2)
        {
            if (m1 == m2) // This will check currency.
                return m1;

            return new Money(m1.Currency, Math.Min(m1.Value, m2.Value));
        }

        /// <summary>
        /// Gets the largest money, given the two values.
        /// </summary>
        /// <param name="m1">The first money to compare.</param>
        /// <param name="m2">The second money to compare.</param>
        /// <returns>The largest money value of the arguments.</returns>
        public static Money Max(Money m1, Money m2)
        {
            if (m1 == m2) // This will check currency.
                return m1;

            return new Money(m1.Currency, Math.Max(m1.Value, m2.Value));
        }

        /// <summary>
        /// Gets the absolute value of the <see cref="Money"/>.
        /// </summary>
        /// <param name="value">The value of money to convert.</param>
        /// <returns>The money value as an absolute value.</returns>
        public static Money Abs(Money value)
        {
            return new Money(value.Currency, Math.Abs(value.Value));
        } 

        #endregion
    }
}